/*
 * TxObjectProxyFactory.java   0.0.1    30/ott/2009
 * 
 * Copyright (c) 2009 mentalsmash.org
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The use of the Apache License does not indicate that this project is
 * affiliated with the Apache Software Foundation.
 */
package org.mentalsmash.tazio.transactions.objects;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.mentalsmash.documentation.Concurrency;
import org.mentalsmash.tazio.IllegalArgumentException;
import org.mentalsmash.tazio.commons.identifiers.OID;
import org.mentalsmash.tazio.transactions.PropertyGetter;
import org.mentalsmash.tazio.transactions.PropertySetter;
import org.mentalsmash.tazio.transactions.RollbackException;
import org.mentalsmash.tazio.transactions.Transactional;
import org.mentalsmash.tazio.transactions.util.Utility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Preconditions;
import com.google.inject.Inject;

/**
 * Factory class for {@link TxObjectWrapper}. Since the wrapping of an @ {@link Transactional}
 * object is quite complex, this factory has been introduced to pull this
 * complexity out from the class {@link TxObjectWrapper} .
 * 
 * @author Andrea Reale
 * @version 0.0.1
 * @synch All the fields in the class are immutable. No synchronization needed
 *        on the object to wrap, since this class accesses only static
 *        reflective informations.
 */

@Concurrency(concurrentAccess = true, specification = "All the fields in the class are immutable. "
	+ "No synchronization needed on the object to wrap, "
	+ "since this class accesses only static" + "reflective informations.")
class TxObjectWrapperFactoryImpl implements TxObjectWrapperFactory {

    private static Logger log = LoggerFactory
	    .getLogger(TxObjectWrapperFactoryImpl.class);

    @Inject
    public TxObjectWrapperFactoryImpl() {

    }

    /*
         * (non-Javadoc)
         * 
         * @seeorg.mentalsmash.tazio.transactions.impl.TxObjectWrapperFactory#
         * createTxWrapper(java.io.Serializable)
         */
    @Override
    public TxObjectWrapper createTxWrapper(final OID objectId,
	    final String label, final Serializable target) {
	final long version = TransactionalObject.INITIAL_VERSION;
	final TxObjectWrapper proxy;

	final Map<String, Property> properties = validateTargetObject(target);
	// the next line should be fine since the target was already
	// validated
	// TODO: Optimize
	final Class<?> txInterface = Utility
		.findTransactionalInterfaces(target)[0];
	proxy = new TxObjectWrapper(objectId, label, version, target,
		txInterface, properties);

	return proxy;
    }

    /**
         * Validates an object for its "proxability" and extracts a map of its
         * transactional properties by reading the method annotations of its
         * interfaces annotated with {@link Transactional}
         * 
         * @param target
         *                the object to proxy
         * @return a map of properties for the given <code>target</code>
         * @throws InvalidTransactionalObjectException
         *                 if the format of the interface marked with the
         *                 {@link Transactional} annotation is not valid
         */
    private Map<String, Property> validateTargetObject(final Serializable target) {
	final Class<?> everyTxInterfaces[] = Utility
		.findTransactionalInterfaces(target);
	final Class<?> txInterface;
	final Map<String, Property> properties = new HashMap<String, Property>();

	if (everyTxInterfaces == null) {
	    log.error("Target object {} does not implement any interface "
		    + "annotaed with @Transactional");
	} else if (everyTxInterfaces.length != 1) {
	    log.error("Target object {} implements more"
		    + " then one interface annotaed with"
		    + "@Transactional annotation");
	    throw new InvalidTransactionalObjectException(""
		    + "The target object " + target + " implements"
		    + " more then one interface annotated with the "
		    + "@Transactional annotation");
	}
	txInterface = everyTxInterfaces[0];

	fillInProperties(txInterface, properties);
	return properties;

    }

    /**
         * Fills in the map of properties for the object wrapped by this proxy
         * 
         * @throws InvalidTransactionalObjectException
         *                 if the transactional interface presents something
         *                 wrong
         */
    private void fillInProperties(final Class<?> iface,
	    final Map<String, Property> properties) {

	for (final Method method : iface.getMethods()) {
	    if (method.isAnnotationPresent(PropertyGetter.class)) {
		addProperty(iface, method, properties);
	    }
	}

    }

    /**
         * Adds a property to the map of properties given a getter method of the
         * transactional interface
         * 
         * @param getterMethod
         *                the method annotated with {@link PropertyGetter} in a
         *                transactional interface
         */
    private void addProperty(final Class<?> iface, final Method getterMethod,
	    final Map<String, Property> properties) {

	Preconditions.checkNotNull(getterMethod);
	Preconditions.checkNotNull(iface);

	final PropertyGetter getterAnnotation = getterMethod
		.getAnnotation(PropertyGetter.class);
	final String propertyName;
	final Method setterMethod;
	if (getterAnnotation == null) {
	    throw new IllegalArgumentException(
		    "Invalid argument: method is not correctly annotated");
	}
	// check if the method correctly declares a Rollback exception
	List<Class<?>> exceptions = Arrays.asList(getterMethod
		.getExceptionTypes());
	if (!exceptions.contains(RollbackException.class)) {
	    throw new InvalidTransactionalObjectException(
		    "Getter and setter methods must throw a RollbackException");
	}

	propertyName = getterAnnotation.propertyName();

	// check whether it is a duplicate entry
	if (properties.containsKey(propertyName)) {
	    log.error("Duplicate getter for property {} in interface {}",
		    propertyName, iface.getSimpleName());
	    throw new InvalidTransactionalObjectException(
		    "Duplicate getter for property " + propertyName
			    + " in interface " + iface.getSimpleName());
	}

	if (!getterAnnotation.readOnly()) { // if not readonly I need to
                                                // find
	    // the corresponding setter method
	    setterMethod = findSetterMethod(iface, propertyName);
//		 check if the method correctly declares a Rollback exception
		exceptions = Arrays.asList(setterMethod
			.getExceptionTypes());
		if (!exceptions.contains(RollbackException.class)) {
		    throw new InvalidTransactionalObjectException(
			    "Getter and setter methods must throw a RollbackException");
		}
	    if (setterMethod == null) {
		log.error("No setter for the property named {} "
			+ "in the interface {}", propertyName, iface
			.getSimpleName());
		throw new InvalidTransactionalObjectException(
			"No valid Property setter " + "was found for property "
				+ propertyName + " in the interface "
				+ iface.getSimpleName());
	    }
	} else { // if the property is readonly
	    setterMethod = null;
	}
	


	try {
	    properties.put(propertyName, new Property(propertyName,
		    getterMethod, setterMethod));
	} catch (final IllegalPropertyException e) {
	    log.error(e.getMessage(), e);
	    throw new InvalidTransactionalObjectException(e);
	}

    }

    /**
         * Finds a method annotated with {@link PropertySetter} and having name
         * <code>propertyName</code> in the interface <code>iface</code> and
         * returns it
         * 
         * @param propertyName
         *                the name of the property as expressed in the
         *                annotation
         * @return a reference to the method
         * @throws InvalidTransactionalObjectException
         *                 if there is more then one setter for a property named
         *                 <code>propertyName</code>
         */
    private Method findSetterMethod(final Class<?> iface,
	    final String propertyName) {

	Method toReturn = null;
	boolean found = false;

	for (final Method method : iface.getMethods()) {
	    if (method.isAnnotationPresent(PropertySetter.class)) {
		final PropertySetter annotation = method
			.getAnnotation(PropertySetter.class);
		if (annotation.propertyName().equals(propertyName)) {
		    if (!found) {
			toReturn = method;
			found = true;
		    } else { // check that there is exactly one setter for a
			// property
			log.error(
				"More then one PropertySetter method for property named "
					+ "{} in interface {}", propertyName,
				iface.getSimpleName());
			throw new InvalidTransactionalObjectException(
				"More then one PropertySetter method for property named "
					+ propertyName + " in interface "
					+ iface.getSimpleName());
		    }
		}
	    }
	}

	return toReturn;

    }

    /**
         * A proxied object is considered valid by this method if it is not
         * <code>null</code>, it is not already an instance of
         * {@link TxObjectWrapper} , and one of its interfaces is annotated with
         * the {@link Transactional} annotation
         * 
         * @param obj
         *                the instance to check
         * @return true if the object is valid
         */
    @SuppressWarnings("unused")
    private boolean isValidWrappedObject(final Serializable obj) {
	final Class<?>[] txInterfaces;

	if (obj == null) {
	    return false;
	}
	if (obj instanceof TxObjectWrapper) {
	    return false;
	}

	txInterfaces = Utility.findTransactionalInterfaces(obj);
	if (txInterfaces.length != 1) {
	    return false;
	}

	return true;
    }

}
